x
1
1
#Mini-portable shell - new
  • boot
    • base.d.ts
      declare function getText(element: Element): string;
      declare function getText(fn: Function): string;
      
      declare function setText(element: Element, text: string): void;
      
      declare function elem(tag: string): HTMLElement;
      declare function elem(tag: string, style: {}, parent?: Element): HTMLElement;
      declare function elem(tag: string, parent: Element): HTMLElement;
      declare function elem(elem: HTMLElement, style: {}, parent?: Element): HTMLElement;
      
      declare function createFrame(style?: {}):
        { global: Window; document: Document; iframe: HTMLIFrameElement; };
      
      declare function loadMod(
        options: {
      
          /** module script */
          eval: string;
      
          /** module path to emulate */
          path?: string;
      
          /** style or class name for the injected iframe (not needed for headless) */
          style?: {} | string;
      
          /** scope to inject, or a function to create such scope using the existing global of the injected iframe */
          scope?: {} | ((global: any) => {});
      
          /** whether to show the iframe (false to delete iframe immediately) */
          ui?: boolean;
      
        }): { global: any; document: Document; iframe: HTMLIFrameElement; };
      
      declare module loadMod {
        export interface LoadedResult {
          global: any;
          document: Document;
          iframe: HTMLIFrameElement
        }
      }
    • base.js
      function getText(obj) {
      
        if (typeof obj === 'function') {
          var result = /\/\*(\*(?!\/)|[^*])*\*\//m.exec(obj+'')[0];
          if (result) result = result.slice(2, result.length-2);
          return result;
        }
        else if (/^SCRIPT$/i.test(obj.tagName)) {
          if ('text' in obj)
            return obj.text;
          else
            return obj.innerHTML;
        }
        else if (/^STYLE$/i.test(obj.tagName)) {
          if ('text' in obj)
            return obj.text;
          else if (obj.styleSheet)
            return obj.styleSheet.cssText;
          else
            return obj.innerHTML;
        }
        else if ('textContent' in obj) {
          return obj.textContent;
        }
        else if (/^INPUT$/i.test(obj.tagName)) {
          return obj.value;
        }
        else {
          var result = obj.innerText;
          if (result) {
            // IE fixes
            result = result.replace(/\<BR\s*\>/g, '\n').replace(/\r\n/g, '\n');
          }
          return result || '';
        }
      }
      
      function setText(obj, text) {
      
        if (/^SCRIPT$/i.test(obj.tagName)) {
          if ('text' in obj)
            obj.text = text;
          else
            obj.innerHTML = text;
        }
        else if (/^STYLE$/i.test(obj.tagName)) {
          if ('text' in obj)
            obj.text = text;
          else if (obj.styleSheet)
            obj.styleSheet.cssText = text;
          else
            obj.innerHTML = text;
        }
        else if ('textContent' in obj) {
          obj.textContent = text;
        }
        else if (/^INPUT$/i.test(obj.tagName)) {
          obj.value = text;
        }
        else {
          obj.innerText = text;
        }
      }
      
      function elem(tag, style, parent) {
        var e = tag.tagName ? tag : this.document.createElement(tag);
      
        if (!parent && style && style.tagName) {
          parent = style;
          style = null;
        }
      
        if (style) {
          if (typeof style === 'string') {
            setText(e, style);
          }
          else {
            for (var k in style) if (style.hasOwnProperty(k)) {
              if (k === 'text') {
                setText(e, style[k]);
              }
              else if (k === 'className') {
                e.className = style[k];
              }
              else if (!(k in e.style) && k in e) {
                e[k] = style[k];
              }
              else {
                try {
                  e.style[k] = style[k];
                }
                catch (err) {
                  try {
                    if (typeof console !== 'undefined' && typeof console.error === 'function')
                      console.error(e.tagName+'.style.'+k+'='+style[k]+': '+err.message);
                  }
                  catch (whatevs) {
                    alert(e.tagName+'.style.'+k+'='+style[k]+': '+err.message);
                  }
                }
              }
            }
          }
        }
      
        if (parent) {
          try {
            parent.appendChild(e);
          }
          catch (error) {
            throw new Error(error.message+' adding '+e.tagName+' to '+parent.tagName);
          }
        }
      
        return e;
      }
      
      function createFrame(style) {
      
        if (!style)
          style = {
            position: 'absolute',
            left: 0, top: 0,
            width: '100%', height: '100%',
            border: 'none',
            src: 'about:blank'
          };
      
        var ifr = this.elem('iframe', style, this.document.body);
      
        var ifrwin = ifr.contentWindow || ifr.window;
        var ifrdoc = ifrwin.document;
      
        if (ifrdoc.open) ifrdoc.open();
        ifrdoc.write(
          '<!'+'doctype html'+'>'+
          '<'+'html'+'>'+
          '<'+'head'+'><'+'style'+'>'+
          'body,html{margin:0;padding:0;border:none;height:100%;}'+
          '*,*:before,*:after{box-sizing:inherit;}'+
          'html{box-sizing:border-box;}'+
          '</'+'style'+'>\n'+
          '<'+'body'+'><'+'body'+'>'+
        	'</'+'html'+'>');
        if (ifrdoc.close) ifrdoc.close();
      
        ifrwin.elem = elem;
      
        return {
          document: ifrdoc,
          global: ifrwin,
          iframe: ifr
        };
      }
      
      function loadMod(options) {
      
        var style = options.style;
        if (!options.ui) {
          style = { display: 'none' };
        }
        else if (typeof style === 'string') {
          style = { className: style, display: 'none' };
        }
      
        var frame = this.createFrame(style);
        var frameFunction = frame.global.Function;
      
        if (options.scope) {
          var scope = typeof options.scope === 'function' ? (options.scope)(frame.global) : options.scope;
          for (var k in scope) if (scope.hasOwnProperty(k)) {
            frame.global[k] = scope[k];
          }
        }
      
        if (options.eval) {
      
          var exportsInScope = scope && 'exports' in scope;
          var evalArgNames = exportsInScope ? [] : ['exports'];
          var evalArgs = exportsInScope ? [] : [{}];
          if (scope) {
            for (var k in scope) if (scope.hasOwnProperty(k)) {
              evalArgNames.push(k);
              evalArgs.push(scope[k]);
            }
          }
      
          if (!options.ui) {
            var allowedGlobals = {
              setTimeout: 1, setInterval: 1, clearTimeout: 1, clearInterval: 1,
              eval: 1,
              console: 1,
              undefined: 1,
              Array: 1, Date: 1, Function: 1, String: 1, Boolean: 1, Number: 1,
              Infinity: 1, NaN: 1, isNaN: 1, isFinite: 1, parseInt: 1, parseFloat: 1,
              escape: 1, unescape: 1,
      
              Int32Array: 1, Int8Array: 1, Int16Array: 1,
              UInt32Array: 1, UInt8Array: 1, UInt8ClampedArray: 1, UInt16Array: 1,
              Float32Array: 1, Float64Array: 1, ArrayBuffer: 1,
      
              Math: 1, JSON: 1, RegExp: 1,
              Error: 1, SyntaxError: 1, EvalError: 1, RangeError: 1, ReferenceError: 1,
      
              toString: 1, toJSON: 1, toValue: 1,
      
              Map: 1
            };
      
            var hiddenKeys = {};
      
            // normal properties
            for (var k in frame.global) {
              if (scope && scope.hasOwnProperty(k)) continue;
              if (allowedGlobals.hasOwnProperty(k)) continue;
              evalArgNames.push(k);
              hiddenKeys[k] = 1;
            }
      
            // non-enumerable properties directly on global
            if (Object.getOwnPropertyNames) {
              var props = Object.getOwnPropertyNames(frame.global);
              for (var i = 0; i < props.length; i++) {
                if (scope && scope.hasOwnProperty(props[i])) continue;
                if (allowedGlobals.hasOwnProperty(props[i])) continue;
                if (hiddenKeys.hasOwnProperty(props[i])) continue;
                evalArgNames.push(props[i]);
              }
      
              // non-enumerable properties on global's prototype
              if (frame.global.constructor
                && frame.global.constructor.prototype
                && frame.global.constructor.prototype != Object
                && frame.global.constructor.prototype != Object.prototype) {
                props = Object.getOwnPropertyNames(frame.global.constructor.prototype);
                for (var i = 0; i < props.length; i++) {
                  if (scope && scope.hasOwnProperty(props[i])) continue;
                  if (allowedGlobals.hasOwnProperty(props[i])) continue;
                  if (hiddenKeys.hasOwnProperty(props[i])) continue;
                  evalArgNames.push(props[i]);
                }
              }
            }
          }
      
          evalArgNames.push(
            options.path ? options.eval + '\nreturn exports; //# sourceURL=' + options.path : options.eval);
      
          var evalFn = frameFunction.apply(frame.global, evalArgNames);
      
          var modExports = evalFn.apply(frame.global, evalArgs);
      
          return {
            document: frame.document,
            global: frame.global,
            iframe: frame.iframe,
            exports: modExports
          };
        }
      
        return frame;
      
      }
    • bootUI.js
      function bootUI(document, window, elem) {
      
        elem(document.body, {
          background: 'royalblue'
        });
      
        var header = elem('h2', { text: 'Loading...' }, document.body);
      
        return {
          loaded: function() {
            setText(header, 'Loaded.');
          }
        };
      }
    • earlyBoot.js
      function earlyBoot() {
      
        document.write(
          '<'+'style'+'>'+
          '*{display:none;background:white;color:white;}'+
          'html,body,iframe{display:block;}'+
          '</'+'style'+'>'+
          (document.body ? '' : '<body>'));
      
        var bootFrame = createFrame();
      
        bootFrame.global.elem = elem;
      
        var bootAPI = bootUI(bootFrame.document, bootFrame.global, function elemProxy(a,b,c) { return bootFrame.global.elem(a,b,c); });
        bootFrame.api = bootAPI;
      
        var uniqueKey = deriveUniqueKey(location);
      
        var shellLoaderInstance = null;
      	var shellLoadInterval = setInterval(function() {
          shellLoaderInstance = shellLoaderInstance ? shellLoaderInstance.continueLoading() : shellLoader(uniqueKey, document);
        }, 100);
      
        window.onload = function() {
      
          clearInterval(shellLoadInterval);
      
          (shellLoaderInstance || shellLoader(uniqueKey, document, bootFrame)).finishLoading();
      
        };
      
        function deriveUniqueKey(locationSeed) {
          var key = (locationSeed + '').split('?')[0].split('#')[0].toLowerCase();
      
          var posIndexTrail = key.search(/\/index\.html$/);
          if (posIndexTrail>0) key = key.slice(0, posIndexTrail);
      
          if (key.charAt(0) === '/')
            key = key.slice(1);
          if (key.slice(-1) === '/')
            key = key.slice(0, key.length - 1);
      
          return smallHash(key) + '-' + smallHash(key.slice(1) + 'a');
      
          function smallHash(key) {
            for (var h=0, i=0; i < key.length; i++) {
              h = Math.pow(31, h + 31 / key.charCodeAt(i));
              h -= h | 0;
            }
            return (h * 2000000000) | 0;
          }
      
        }
      
      }
    • onerror.js
      window.onerror = function onerror() {
      
        var msg = [];
        for (var i = 0; i < arguments.length; i++) {
          var a = arguments[i];
          if (a && (typeof a === 'object')) {
      
            if (a.stack) {
              msg.push(a.stack);
            }
            else {
              var msg1 = [];
              for (var k in a) {
                var r = a[k];
                if (typeof r === 'function' || (typeof r === 'object' && !r)) continue;
                msg1.push(k+':'+r);
              }
              msg.push(msg1.join(', '));
            }
          }
          else {
            msg.push(a===null ? 'null' : a);
          }
      
        }
      
        alert(msg.join('\n'));
      
      }
  • load
    • shellLoader.ts
      function shellLoader(uniqueKey: string, document: Document, boot: shellLoader.BootModuleAPI): shellLoader.ContinueLoading {
      
        var driveMount = persistence.bootMount(uniqueKey, document);
      
        return continueLoading();
      
        function continueLoading(): shellLoader.ContinueLoading {
          driveMount.continueLoading();
          return { continueLoading, finishLoading };
        }
      
        function finishLoading() {
      
          driveMount.finishLoading(drive => {
            // EVERYTHING READY!
      
            console.log(window['dbgDrive'] = drive);
      
            elem(boot.document.body, {
              background: 'white',
              color: 'black'
            });
            var coolLoadedTitle = elem('h1', {
              text: 'Finished.'
            }, boot.document.body);
          });
      
        }
      
      }
      
      module shellLoader {
      
        export interface BootModuleAPI extends loadMod.LoadedResult {
          api: any;
        }
      
        export interface ContinueLoading {
      
          continueLoading(): ContinueLoading;
      
          finishLoading();
      
        }
      
      }
  • persistence
    • attached
      • indexedDB.ts
        module persistence {
        
          function getIndexedDB() {
            try {
              return typeof indexedDB === 'undefined' || typeof indexedDB.open !== 'function' ? null : indexedDB;
            }
            catch (error) {
              return null;
            }
          }
        
          export module attached.indexedDB {
        
            export var name = 'indexedDB';
        
            export function detect(uniqueKey: string, callback: (detached: Drive.Detached) => void): void {
              try {
                detectCore(uniqueKey, callback);
              }
              catch (error) {
                callback(null);
              }
            }
        
            function detectCore(uniqueKey: string, callback: (detached: Drive.Detached) => void): void {
        
              var indexedDBInstance = getIndexedDB();
              if (!indexedDBInstance) {
                callback(null);
                return;
              }
        
              var dbName = uniqueKey || 'portabled';
        
              var openRequest = indexedDBInstance.open(dbName, 1);
              openRequest.onerror = (errorEvent) => callback(null);
        
              openRequest.onupgradeneeded = createDBAndTables;
        
              openRequest.onsuccess = (event) => {
                var db: IDBDatabase = openRequest.result;
        
                try {
                  var transaction = db.transaction(['files', 'metadata']);
                  // files mentioned here, but not really used to detect
                  // broken multi-store transaction implementation in Safari
        
                  transaction.onerror = (errorEvent) => callback(null);
        
                  var metadataStore = transaction.objectStore('metadata');
                  var filesStore = transaction.objectStore('files');
                  var editedUTCRequest = metadataStore.get('editedUTC');
                }
                catch (getStoreError) {
                  callback(null);
                  return;
                }
        
                if (!editedUTCRequest) {
                  callback(null);
                  return;
                }
        
                editedUTCRequest.onerror = (errorEvent) => {
                  var detached = new IndexedDBDetached(db, null);
                  callback(detached);
                };
        
                editedUTCRequest.onsuccess = (event) => {
                  var result: MetadataData = editedUTCRequest.result;
                  var detached = new IndexedDBDetached(db, result && typeof result.value === 'number' ? result.value : null);
                  callback(detached);
                };
              }
        
        
              function createDBAndTables() {
                var db: IDBDatabase = openRequest.result;
                var filesStore = db.createObjectStore('files', { keyPath: 'path' });
                var metadataStore = db.createObjectStore('metadata', { keyPath: 'property' })
              }
            }
        
        
        
            class IndexedDBDetached implements Drive.Detached {
        
              constructor(
                private _db: IDBDatabase,
                public timestamp: number) {
              }
        
              applyTo(mainDrive: Drive, callback: Drive.Detached.CallbackWithShadow): void {
                var transaction = this._db.transaction(['files', 'metadata'], 'readwrite');
                var metadataStore = transaction.objectStore('metadata');
                var filesStore = transaction.objectStore('files');
        
                var countRequest = filesStore.count();
                countRequest.onerror = (errorEvent) => {
                  console.error('Could not count files store.');
                  callback(null);
                };
        
                countRequest.onsuccess = (event) => {
        
                  var storeCount: number = countRequest.result;
        
                  var cursorRequest = filesStore.openCursor();
                  cursorRequest.onerror = (errorEvent) => callback(null);
        
                  // to cleanup any files which content is the same on the main drive
                  var deleteList: string[] = [];
                  var anyLeft = false;
        
                  var processedCount = 0;
        
                  cursorRequest.onsuccess = (event) => {
                    var cursor: IDBCursor = cursorRequest.result;
        
                    if (!cursor) {
        
                      // cleaning up files whose content is duplicating the main drive
                      if (anyLeft) {
                        for (var i = 0; i < deleteList.length; i++) {
                          filesStore['delete'](deleteList[i]);
                        }
                      }
                      else {
                        filesStore.clear();
                        metadataStore.clear();
                      }
        
                      callback(new IndexedDBShadow(this._db, this.timestamp));
                      return;
                    }
        
                    if (callback.progress)
                      callback.progress(processedCount, storeCount);
                    processedCount++;
        
                    var result: FileData = (<any>cursor).value;
                    if (result && result.path) {
        
                      var existingContent = mainDrive.read(result.path);
                      if (existingContent === result.content) {
                        deleteList.push(result.path);
                      }
                      else {
                        mainDrive.timestamp = this.timestamp;
                        mainDrive.write(result.path, result.content);
                        anyLeft = true;
                      }
                    }
        
                    cursor['continue']();
                  }; // cursorRequest.onsuccess
        
                }; // countRequest.onsuccess
        
              }
        
              purge(callback: Drive.Detached.CallbackWithShadow): void {
                var transaction = this._db.transaction(['files', 'metadata'], 'readwrite');
        
                var filesStore = transaction.objectStore('files');
                filesStore.clear();
        
                var metadataStore = transaction.objectStore('metadata');
                metadataStore.clear();
        
                callback(new IndexedDBShadow(this._db, -1));
              }
        
            }
        
            class IndexedDBShadow implements Drive.Shadow {
        
              constructor(private _db: IDBDatabase, public timestamp: number) {
              }
        
              write(file: string, content: string) {
                var transaction = this._db.transaction(['files', 'metadata'], 'readwrite');
                var filesStore = transaction.objectStore('files');
                var metadataStore = transaction.objectStore('metadata');
        
                // no file deletion here: we need to keep account of deletions too!
                var fileData: FileData = {
                  path: file,
                  content: content,
                  state: null
                };
        
                var putFile = filesStore.put(fileData);
        
                var md: MetadataData = {
                  property: 'editedUTC',
                  value: Date.now()
                };
        
                metadataStore.put(md);
        
              }
            }
        
            interface FileData {
              path: string;
              content: string;
              state: string;
            }
        
            interface MetadataData {
              property: string;
              value: any;
            }
        
        
          }
        
        }
      • localStorage.ts
        module persistence {
        
          function getLocalStorage() {
            return typeof localStorage === 'undefined' || typeof localStorage.length !== 'number' ? null : localStorage;
          }
        
          // is it OK&
          export module attached.localStorage {
        
            export var name = 'localStorage';
        
            export function detect(uniqueKey: string, callback: (detached: Drive.Detached) => void): void {
              var localStorageInstance = getLocalStorage();
              if (!localStorageInstance) {
                callback(null);
                return;
              }
        
              var access = new LocalStorageAccess(localStorageInstance, uniqueKey);
              var dt = new LocalStorageDetached(access);
              callback(dt);
            }
        
            class LocalStorageAccess {
              private _cache: { [key: string]: string; } = {};
        
              constructor(private _localStorage: Storage, private _prefix: string) {
              }
        
              get (key: string): string {
                var k = this._expandKey(key);
                var r = this._localStorage.getItem(k);
                return r;
              }
            
            	set(key: string, value: string): void {
                var k = this._expandKey(key);
                return this._localStorage.setItem(k, value);
              }
        
              remove(key: string): void {
                var k = this._expandKey(key);
                return this._localStorage.removeItem(k);
              }
        
              keys(): string[] {
                var result: string[] = [];
                var len = this._localStorage.length;
                for (var i = 0; i < len; i++) {
                  var str = this._localStorage.key(i);
                  if (str.length > this._prefix.length && str.slice(0, this._prefix.length) === this._prefix)
                    result.push(str.slice(this._prefix.length));
                }
                return result;
              }
        
              private _expandKey(key: string): string {
                var k: string;
        
                if (!key) {
                  k = this._prefix;
                }
                else {
                  k = this._cache[key];
                  if (!k)
                    this._cache[key] = k = this._prefix + key;
                }
                
                return k;
              }
          	}
        
        
            class LocalStorageDetached implements Drive.Detached {
        
              timestamp: number = 0;
        
              constructor(private _access: LocalStorageAccess) {
                var timestampStr = this._access.get('*timestamp');
                if (timestampStr && timestampStr.charAt(0)>='0' && timestampStr.charAt(0)<='9') {
                  try {
                    this.timestamp = parseInt(timestampStr);
                  }
                  catch (parseError) {
                  }
                }
              }
        
              applyTo(mainDrive: Drive, callback: Drive.Detached.CallbackWithShadow): void {
                var keys = this._access.keys();
                for (var i = 0; i < keys.length; i++) {
                  var k = keys[i];
                  if (k.charAt(0)==='/') {
                    var value = this._access.get(k);
                    mainDrive.write(k, value);
                  }
                }
                
                var shadow = new LocalStorageShadow(this._access, mainDrive.timestamp);
                callback(shadow);
              }
        
              purge(callback: Drive.Detached.CallbackWithShadow): void {
                var keys = this._access.keys();
                for (var i = 0; i < keys.length; i++) {
                  var k = keys[i];
                  if (k.charAt(0)==='/') {
                    var value = this._access.remove(k);
                  }
                }
        
                var shadow = new LocalStorageShadow(this._access, this.timestamp);
                callback(shadow);
              }
        
            }
            
            class LocalStorageShadow implements Drive.Shadow {
        
              constructor(private _access: LocalStorageAccess, public timestamp: number) {
              }
        
              write(file: string, content: string) {
                this._access.set(file, content);
                this._access.set('*timestamp', <any>this.timestamp);
              }
        
            }
        
          }
          
        } 
      • webSQL.ts
        module persistence {
        
          function getOpenDatabase() {
            return typeof openDatabase !== 'function' ? null : openDatabase;
          }
        
          export module attached.webSQL {
        
            export var name = 'webSQL';
        
            export function detect(uniqueKey: string, callback: (detached: Drive.Detached) => void): void {
        
              var openDatabaseInstance = getOpenDatabase();
              if (!openDatabaseInstance) {
                callback(null);
                return;
              }
        
              var dbName = uniqueKey || 'portabled';
        
              var db = openDatabase(
                dbName, // name
                1, // version
                'Portabled virtual filesystem data', // displayName
                1024 * 1024); // size
              // upgradeCallback?
        
        
              db.readTransaction(
                transaction => {
                  transaction.executeSql(
                    'SELECT value from "*metadata" WHERE name=\'editedUTC\'',
                    [],
                    (transaction, result) => {
                      var editedValue: number = null;
                      if (result.rows && result.rows.length === 1) {
                        var editedValueStr = result.rows.item(0).value;
                        if (typeof editedValueStr === 'string') {
                          try {
                            editedValue = parseInt(editedValueStr);
                          }
                          catch (error) {
                            // unexpected value for the timestamp, continue as if no value found
                          }
                        }
                        else if (typeof editedValueStr === 'number') {
                          editedValue = editedValueStr;
                        }
                      }
        
                      callback(new WebSQLDetached(db, editedValue || 0, true));
                    },
                    (transaction, sqlError) => {
                      // no data
                      callback(new WebSQLDetached(db, 0, false));
                    });
                },
                sqlError=> {
                  // failed to load
                  callback(null);
                });
        
            }
        
            class WebSQLDetached implements Drive.Detached {
        
              constructor(
                private _db: Database,
                public timestamp: number,
                private _metadataTableIsValid: boolean) {
              }
        
              applyTo(mainDrive: Drive, callback: Drive.Detached.CallbackWithShadow): void {
                this._db.readTransaction(
                  transaction => listAllTables(
                    transaction,
                    tables => {
        
                      var ftab = getFilenamesFromTables(tables);
        
                      this._applyToWithFiles(transaction, ftab, mainDrive, callback);
                    },
                    sqlError => {
                      reportSQLError('Failed to list tables for the webSQL database.', sqlError);
                      callback(new WebSQLShadow(this._db, this.timestamp, this._metadataTableIsValid));
                    }),
                  sqlError => {
                    reportSQLError('Failed to open read transaction for the webSQL database.', sqlError);
                    callback(new WebSQLShadow(this._db, this.timestamp, this._metadataTableIsValid));
                  });
              }
        
              purge(callback: Drive.Detached.CallbackWithShadow): void {
                this._db.transaction(
                  transaction => listAllTables(
                    transaction,
                    tables => {
                      this._purgeWithTables(transaction, tables, callback);
                    },
                    sqlError => {
                      reportSQLError('Failed to list tables for the webSQL database.', sqlError);
                      callback(new WebSQLShadow(this._db, 0, false));
                    }),
                  sqlError => {
                    reportSQLError('Failed to open read-write transaction for the webSQL database.', sqlError);
                    callback(new WebSQLShadow(this._db, 0, false));
                  });
              }
        
              private _applyToWithFiles(transaction: SQLTransaction, ftab: { file: string; table: string; }[], mainDrive: Drive, callback: Drive.Detached.CallbackWithShadow): void {
        
                if (!ftab.length) {
                  callback(new WebSQLShadow(this._db, this.timestamp, this._metadataTableIsValid));
                  return;
                }
        
                var reportedFileCount = 0;
        
                var completeOne = () => {
                  reportedFileCount++;
                  if (reportedFileCount === ftab.length) {
                    callback(new WebSQLShadow(this._db, this.timestamp, this._metadataTableIsValid));
                  }
                };
        
                var applyFile = (file: string, table: string) => {
                  transaction.executeSql(
                    'SELECT * FROM "' + table + '"',
                    [],
                    (transaction, result) => {
                      if (result.rows.length) {
                        var row = result.rows.item(0);
                        if (row.value === null)
                          mainDrive.write(file, null);
                        else if (typeof row.value === 'string')
                          mainDrive.write(file, fromSqlText(row.value));
                      }
                      completeOne();
                    },
                    sqlError => {
                      completeOne();
                    });
                };
        
                for (var i = 0; i < ftab.length; i++) {
                  applyFile(ftab[i].file, ftab[i].table);
                }
        
              }
        
              private _purgeWithTables(transaction: SQLTransaction, tables: string[], callback: Drive.Detached.CallbackWithShadow) {
                if (!tables.length) {
                  callback(new WebSQLShadow(this._db, 0, false));
                  return;
                }
        
                var droppedCount = 0;
        
                var completeOne = () => {
                  droppedCount++;
                  if (droppedCount === tables.length) {
                    callback(new WebSQLShadow(this._db, 0, false));
                  }
                };
        
                for (var i = 0; i < tables.length; i++) {
                  transaction.executeSql(
                    'DROP TABLE "' + tables[i] + '"',
                    [],
                    (transaction, result) => {
                      completeOne();
                    },
                    (transaction, sqlError) => {
                      reportSQLError('Failed to drop table for the webSQL database.', sqlError);
                      completeOne();
                    });
                }
              }
        
            }
        
            class WebSQLShadow implements Drive.Shadow {
        
              private _cachedUpdateStatementsByFile: { [name: string]: string; } = {};
              private _closures = {
                updateMetadata: (transaction: SQLTransaction) => this._updateMetadata(transaction)
              };
        
              constructor(private _db: Database, public timestamp: number, private _metadataTableIsValid: boolean) {
              }
        
              write(file: string, content: string) {
        
                if (content || typeof content === 'string') {
                  this._updateCore(file, content);
                }
                else {
                  this._dropFileTable(file);
                }
              }
        
              private _updateCore(file: string, content: string) {
                var updateSQL = this._cachedUpdateStatementsByFile[file];
                if (!updateSQL) {
                  var tableName = mangleDatabaseObjectName(file);
                  updateSQL = this._createUpdateStatement(file, tableName);
                }
                this._db.transaction(
                  transaction => {
                    transaction.executeSql(
                      updateSQL,
                      ['content', content],
                      this._closures.updateMetadata,
                      (transaction, sqlError) => this._createTableAndUpdate(transaction, file, tableName, updateSQL, content));
                  },
                  sqlError => {
                    reportSQLError('Transaction failure updating file "' + file + '".', sqlError);
                  });
              }
        
              private _createTableAndUpdate(transaction: SQLTransaction, file: string, tableName: string, updateSQL: string, content: string) {
                if (!tableName)
                  tableName = mangleDatabaseObjectName(file);
        
                transaction.executeSql(
                  'CREATE TABLE "' + tableName + '" (name PRIMARY KEY, value)',
                  [],
                  (transaction, result) => {
                    transaction.executeSql(
                      updateSQL,
                      ['content', content],
                      this._closures.updateMetadata,
                      (transaction, sqlError) => {
                        reportSQLError('Failed to update table "' + tableName + '" for file "' + file + '" after creation.', sqlError);
                      });
                  },
                  (transaction, sqlError) => {
                    reportSQLError('Failed to create a table "' + tableName + '" for file "' + file + '".', sqlError);
                  });
              }
        
              private _dropFileTable(file: string) {
                var tableName = mangleDatabaseObjectName(file);
                this._db.transaction(
                  transaction => {
                    transaction.executeSql(
                      'DROP TABLE "' + tableName + '"',
                      [],
                      this._closures.updateMetadata,
                      (transaction, sqlError) => {
                        reportSQLError('Failed to drop table "' + tableName + '" for file "' + file + '".', sqlError);
                      });
                  },
                  sqlError => {
                    reportSQLError('Transaction failure dropping table "' + tableName + '" for file "' + file + '".', sqlError);
                  });
              }
        
              private _updateMetadata(transaction: SQLTransaction) {
                var updateMetadataSQL = 'INSERT OR REPLACE INTO "*metadata" VALUES (?,?)';
                transaction.executeSql(
                  updateMetadataSQL,
                  ['editedUTC', this.timestamp],
                  (transaction, result) => { }, // TODO: generate closure statically
                  (transaction, error) => {
                    transaction.executeSql(
                      'CREATE TABLE "*metadata" (name PRIMARY KEY, value)',
                      [],
                      (transaction, result) => {
                        transaction.executeSql(updateMetadataSQL, [], () => { }, () => { });
                      },
                      (transaction, sqlError) => {
                        reportSQLError('Failed to update metadata table after creation.', sqlError);
                      });
                  });
        
              }
        
              private _createUpdateStatement(file: string, tableName: string): string {
                return this._cachedUpdateStatementsByFile[file] =
                  'INSERT OR REPLACE INTO "' + tableName + '" VALUES (?,?)';
              }
            }
        
        
            function mangleDatabaseObjectName(name: string): string {
              // no need to polyfill btoa, if webSQL exists
              if (name.toLowerCase() === name)
                return name;
              else
                return '=' + btoa(name);
            }
        
            function unmangleDatabaseObjectName(name: string): string {
              if (!name || name.charAt(0) === '*') return null;
        
              if (name.charAt(0) !== '=') return name;
        
              try {
                return atob(name.slice(1));
              }
              catch (error) {
                return name;
              }
            }
        
            export function listAllTables(
              transaction: SQLTransaction,
              callback: (tables: string[]) => void,
              errorCallback: (sqlError: SQLError) => void) {
              transaction.executeSql(
                'SELECT tbl_name  from sqlite_master WHERE type=\'table\'',
                [],
                (transaction, result) => {
                  var tables: string[] = [];
                  for (var i = 0; i < result.rows.length; i++) {
                    var row = result.rows.item(i);
                    var table = row.tbl_name;
                    if (!table || (table[0] !== '*' && table.charAt(0) !== '=' && table.charAt(0) !== '/')) continue;
                    tables.push(row.tbl_name);
                  }
                  callback(tables);
                },
                (transaction, sqlError) => errorCallback(sqlError));
            }
        
            function getFilenamesFromTables(tables: string[]) {
              var filenames: { table: string; file: string; }[] = [];
              for (var i = 0; i < tables.length; i++) {
                var file = unmangleDatabaseObjectName(tables[i]);
                if (file)
                  filenames.push({ table: tables[i], file: file });
              }
              return filenames;
            }
        
            function toSqlText(text: string) {
              if (text.indexOf('\u00FF') < 0 && text.indexOf('\u0000') < 0) return text;
        
              return text.replace(/\u00FF/g, '\u00FFf').replace(/\u0000/g, '\u00FF0');
            }
        
            function fromSqlText(sqlText: string) {
              if (sqlText.indexOf('\u00FF') < 0 && sqlText.indexOf('\u0000') < 0) return sqlText;
        
              return sqlText.replace(/\u00FFf/g, '\u00FF').replace(/\u00FF0/g, '\u0000');
            }
        
            function reportSQLError(message: string, sqlError: SQLError);
            function reportSQLError(sqlError: SQLError);
            function reportSQLError(message, sqlError?) {
              if (typeof console !== 'undefined' && typeof console.error === 'function') {
                if (sqlError)
                  console.error(message, sqlError);
                else
                  console.error(sqlError);
              }
            }
        
        
          }
        
        }
    • dom
      • CommentHeader.ts
        module persistence.dom {
        
          export class CommentHeader {
        
            header: string;
            contentOffset: number;
            contentLength: number;
        
            constructor(public node: Comment) {
              var headerLine: string;
              var content: string;
              if (typeof node.substringData === 'function'
                && typeof node.length === 'number') {
                var chunkSize = 128;
        
                if (node.length >= chunkSize) {
                  // TODO: cut chunks off the start and look for newlines
                  var headerChunks: string[] = [];
                  while (headerChunks.length * chunkSize < node.length) {
                    var nextChunk = node.substringData(headerChunks.length * chunkSize, chunkSize);
                    var posEOL = nextChunk.search(/\r|\n/);
                    if (posEOL < 0) {
                      headerChunks.push(nextChunk);
                      continue;
                    }
        
                    this.header = headerChunks.join('') + nextChunk.slice(0, posEOL);
                    this.contentOffset = this.header.length + 1; // if header is separated by a single CR or LF
        
                    if (posEOL === nextChunk.length - 1) { // we may have LF part of CRLF in the next chunk!
                      if (nextChunk.charAt(nextChunk.length - 1) === '\r'
                        && node.substringData((headerChunks.length + 1) * chunkSize, 1) === '\n')
                        this.contentOffset++;
                    }
                    else if (nextChunk.slice(posEOL, posEOL + 2) === '\r\n') {
                      this.contentOffset++;
                    }
        
                    this.contentLength = node.length - this.contentOffset;
                    return;
                  }
        
                  this.header = headerChunks.join('');
                  this.contentOffset = this.header.length;
                  this.contentLength = node.length - content.length;
                  return;
                }
              }
        
              var wholeCommentText = node.nodeValue;
              var posEOL = wholeCommentText.search(/\r|\n/);
              if (posEOL < 0) {
                this.header = wholeCommentText;
                this.contentOffset = wholeCommentText.length;
                this.contentLength = wholeCommentText.length - this.contentOffset;
                return;
              }
        
              this.contentOffset = wholeCommentText.slice(posEOL, posEOL + 2) === '\r\n' ?
                posEOL + 2 : // ends with CRLF
                posEOL + 1; // ends with singular CR or LF
        
              this.header = wholeCommentText.slice(0, posEOL),
              this.contentLength = wholeCommentText.length - this.contentOffset
            }
        
          }
        
        }
      • DOMDrive.ts
        module persistence.dom {
        
          export class DOMDrive implements Drive {
        
            private _byPath: { [path: string]: DOMFile; } = {};
        
            public timestamp: number;
        
            constructor(
              private _totals: DOMTotals,
              files: DOMFile[],
              private _document: DOMDrive.DocumentSubset) {
        
              this.timestamp = this._totals ? this._totals.timestamp : 0;
        
              for (var i = 0; i < files.length; i++) {
                this._byPath[files[i].path] = files[i];
              }
            }
        
            files(): string[] {
        
              if (typeof Object.keys === 'string') {
                var result = Object.keys(this._byPath);
              }
              else {
                var result: string[] = [];
                for (var k in this._byPath) if (this._byPath.hasOwnProperty(k)) {
                  result.push(k);
                }
              }
        
              result.sort();
        
              return result;
            }
        
            read(file: string): string {
              var file = normalizePath(file);
              var f = this._byPath[file];
              if (!f)
                return null;
              else
                return f.read();
            }
        
            write(file: string, content: string) {
        
              var totalDelta = 0;
        
              var file = normalizePath(file);
              var f = this._byPath[file];
        
              if (content === null) {
                // removal
                if (f) {
                  totalDelta -= f.contentLength;
                  f.node.parentElement.removeChild(f.node);
                  delete this._byPath[file];
                }
              }
              else {
                // addition
                if (f) {
                  var lengthBefore = f.contentLength;
                  f.write(content);
                  totalDelta += f.contentLength - lengthBefore;
                }
                else {
                  var comment = document.createComment('');
                  var f = new DOMFile(comment, file, null, 0, 0);
                  f.write(content);
                  this._document.body.appendChild(f.node);
                  totalDelta += f.contentLength;
                }
              }
        
              this._totals.timestamp = this.timestamp;
              this._totals.updateNode();
            }
        
          }
        
          export module DOMDrive {
        
            export interface DocumentSubset {
              body: HTMLBodyElementSubset;
        
              createComment(data: string): Comment;
            }
        
            export interface HTMLBodyElementSubset {
              appendChild(node: Node);
              insertBefore(newChild: Node, refNode?: Node);
              firstChild: Node;
            }
          }
        }
      • DOMFile.ts
        module persistence.dom {
        
          export class DOMFile {
        
            private _encodedPath: string = null;
        
            constructor(
              public node: Comment,
              public path: string,
              private _encoding: (text: string) => any,
              private _contentOffset: number,
              public contentLength: number) {
            }
        
            static tryParse(cmheader: CommentHeader): DOMFile {
        
              //    /file/path/continue
              //    "/file/path/continue"
              //    /file/path/continue   [encoding]
        
              var parseFmt = /^\s*((\/|\"\/)(\s|\S)*[^\]])\s*(\[((\s|\S)*)\])?\s*$/;
              var parsed = parseFmt.exec(cmheader.header);
              if (!parsed) return null; // does not match the format
        
              var filePath = parsed[1];
              var encodingName = parsed[5];
        
              if (filePath.charAt(0) === '"') {
                if (filePath.charAt(filePath.length - 1) !== '"') return null; // unpaired leading quote
                try {
                  if (typeof JSON !== 'undefined' && typeof JSON.parse === 'function')
                    filePath = JSON.parse(filePath);
                  else
                    filePath = eval(filePath); // security doesn't seem to be compromised, input is coming from the same file
                }
                catch (parseError) {
                  return null; // quoted path but wrong format (JSON expected)
                }
              }
        
              var encoding = encodings[encodingName || 'LF'];
              // invalid encoding considered a bogus comment, skipped
              if (encoding)
                return new DOMFile(cmheader.node, filePath, encoding, cmheader.contentOffset, cmheader.contentLength);
        
              return null;
            }
        
        
            read() {
        
              // proper HTML5 has substringDate to read only a chunk
              // (that saves on string memory allocations
              // comparing to fetching the whole text including the file name)
              var contentText = typeof this.node.substringData === 'function' ?
                this.node.substringData(this._contentOffset, 1000000000) :
                this.node.nodeValue.slice(this._contentOffset);
        
              // XML end-comment is escaped when stored in DOM,
              // unescape it back
              var restoredText = contentText.replace(/\-\-\*(\**)\>/g, '--*$1>');
        
              // decode
              var decodedText = this._encoding(restoredText);
        
              // update just in case it's been off
              this.contentLength = decodedText.length;
        
              return decodedText;
            }
        
            write(content: any) {
        
              var encoded = bestEncode(content);
              var protectedText = encoded.content.replace(/\-\-(\**)\>/g, '--*$1>');
        
              if (!this._encodedPath) {
                // most cases path is path,
                // but if enything is weird, it's going to be quoted
                // (actually encoded with JSON format)
                var encp = bestEncode(this.path, true /*escapePath*/);
                this._encodedPath = encp.content;
              }
        
              var leadText = ' ' + this._encodedPath + (encoded.encoding === 'LF' ? '' : ' [' + encoded.encoding + ']') + '\n';
              this.node.nodeValue = leadText + encoded.content;
        
              this.contentLength = content.length;
            }
        
          }
        
        }
      • DOMTotals.ts
        module persistence.dom {
        
          export class DOMTotals {
        
            constructor(
            	public timestamp: number,
            	public totalSize: number,
              private _node: Comment) {
            }
        
            static tryParse(cmheader: CommentHeader): DOMTotals {
        
              // TODO: preserve unknowns when parsing
        
              var parts = cmheader.header.split(',');
              var anythingParsed = false;
              var totalSize = 0;
              var timestamp = 0;
        
              for (var i = 0; i < parts.length; i++) {
        
                // total 234Kb
                // total 23
                // total 6Mb
        
                var totalFmt = /^\s*total\s+(\d*)\s*([KkMm])?b?\s*$/;
                var totalMatch = totalFmt.exec(parts[i]);
                if (totalMatch) {
                  try {
                    var total = parseInt(totalMatch[1]);
                    if ((totalMatch[2] + '').toUpperCase() === 'K')
                      total *= 1024;
                    else if ((totalMatch[2] + '').toUpperCase() === 'M')
                      total *= 1024 * 1024;
                    totalSize = total;
                    anythingParsed = true;
                  }
                  catch (totalParseError) { }
                  continue;
                }
        
                var savedFmt = /^\s*saved\s+(\d+)\s+(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+(\d+)\s+(\d+)\:(\d+)(\:(\d+(\.(\d+))?))\s*$/i;
                var savedMatch = savedFmt.exec(parts[i]);
                if (savedMatch) {
                  var saveDate = savedMatch[1];
                  // 25 Apr 2015 22:52:01.231
                  try {
                    var savedDay = parseInt(savedMatch[1]);
                    var savedMonth = ('Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec').toUpperCase().split('|').indexOf(savedMatch[2]) + 1;
                    var savedYear = parseInt(savedMatch[3]);
                    if (savedYear < 100)
                      savedYear += 2000; // no 19xx notation anymore :-(
                    var savedHour = parseInt(savedMatch[4]);
                    var savedMinute = parseInt(savedMatch[5]);
                    var savedSecond = savedMatch[7] ? parseFloat(savedMatch[7]) : 0;
        
                    timestamp = new Date(savedYear, savedMonth, savedDay, savedHour, savedMinute, savedSecond | 0).valueOf();
                    timestamp += savedSecond - (savedSecond | 0);
                    anythingParsed = true;
                  }
                  catch (savedParseError) { }
                }
        
              }
        
              if (anythingParsed)
                return new DOMTotals(timestamp, totalSize, cmheader.node);
              else
                return null;
            }
        
          	updateNode() {
              // TODO: update the node content
        
              // total 4Kb, saved 25 Apr 2015 22:52:01.231
              var newTotals =
                'total ' + (
                  this.totalSize < 1024 * 2 ? this.totalSize + '' :
                    this.totalSize < 1024 * 1024 * 2 ? ((this.totalSize / 1024) | 0) + 'Kb' :
                      ((this.totalSize / (1024 * 1024)) | 0) + 'Mb') + ', ' +
                'saved ';
              var saveDate = new Date(this.timestamp);
              newTotals +=
              saveDate.getFullYear() + ':' +
              num2(saveDate.getMonth() + 1) + ':';
        
              function num2(n: number) {
                return n <= 9 ? '0' + n : '' + n;
              }
        
            }
        
          }
        
        
        }
      • parseDOMStorage.ts
        module persistence.dom {
        
          export function parseDOMStorage(document: parseDOMStorage.DocumentSubset): parseDOMStorage.ContinueParsing {
        
            var loadedFiles: DOMFile[] = [];
            var loadedTotals: DOMTotals;
            var lastNode: Node;
            var loadedSize = 0;
        
            return continueParsing();
        
            function continueParsing(): parseDOMStorage.ContinueParsing {
        
              continueParsingDOM(false);
        
              return {
                continueParsing,
                finishParsing,
                loadedSize,
                totalSize: loadedTotals ? loadedTotals.totalSize : 0,
                loadedFileCount: loadedFiles.length
              };
        
            }
        
            function finishParsing(): DOMDrive {
        
              continueParsingDOM(true);
        
              if (loadedTotals) {
                loadedTotals.totalSize = loadedSize;
                loadedTotals.updateNode();
              }
        
              var drive = new DOMDrive(loadedTotals, loadedFiles, document);
        
              return drive;
            }
        
            function continueParsingDOM(finish: boolean) {
              if (document.body) {
                if (!lastNode)
                  lastNode = document.body.firstChild;
        
                while (true) {
                  if (!lastNode) return;
                  else if (!finish && lastNode == document.body.lastChild) return;
        
        
                  if (lastNode.nodeType === 8) {
                    processNode(<Comment>lastNode);
                  }
        
                  lastNode = lastNode.nextSibling;
                }
              }
            }
        
            function processNode(node: Comment): boolean {
              var cmheader = new CommentHeader(node);
        
              var file = DOMFile.tryParse(cmheader);
              if (file) {
                loadedFiles.push(file);
                loadedSize += file.contentLength;
                return true;
              }
        
              var totals = DOMFile.tryParse(cmheader);
            }
          }
        
          export module parseDOMStorage {
        
            export interface ContinueParsing {
        
              continueParsing(): ContinueParsing;
        
              finishParsing(): DOMDrive;
        
              loadedFileCount: number;
              loadedSize: number;
              totalSize: number;
        
            }
        
            export interface DocumentSubset extends DOMDrive.DocumentSubset {
              body: HTMLBodyElementSubset;
            }
        
            export interface HTMLBodyElementSubset extends DOMDrive.HTMLBodyElementSubset {
              lastChild: Node;
            }
        
          }
        
        }
    • encodings
      • CR.ts
        module persistence.encodings {
        
          export function CR(text: string): string {
            return text.
              replace(/\r\n|\n/g, '\r').
              replace(/\-\-\*(\**)\>/g, '--$1>');
          }
        
        }
      • CRLF.ts
        module persistence.encodings {
        
          export function CRLF(text: string): string {
            return text.
              replace(/\r|\n/g, '\r\n').
              replace(/\-\-\*(\**)\>/g, '--$1>');
          }
        
        }
      • LF.ts
        module persistence.encodings {
        
          export function LF(text: string): string {
            return text.
              replace(/\r\n|\r/g, '\n').
              replace(/\-\-\*(\**)\>/g, '--$1>');
          }
        
        }
      • base64.ts
        module persistence.encodings {
        
          export function base64(text: string): any {
            // TODO: convert from base64 to text
            // TODO: invent a prefix to signify binary data
            throw new Error('Base64 encoding is not implemented yet.');
          }
        
        }
      • eval.ts
        module persistence.encodings {
        
          export function eval(text: string): any {
            return (0, eval)(text);
          }
        
        }
      • json.ts
        module persistence.encodings {
        
          export function json(text: string): any {
            var result = typeof JSON ==='undefined' ? eval(text) : JSON.parse(text);
        
            if (result && typeof result !== 'string' && result.type) {
              var ctor: any = window[result.type];
              result = new ctor(result);
            }
        
            return result;
          }
        
        }
    • Drive.ts
      module persistence {
      
        export interface Drive {
      
          timestamp: number;
      
          files(): string[];
      
          read(file: string): string;
      
          write(file: string, content: string);
      
        }
      
        export module Drive {
      
          export interface Shadow {
      
            timestamp: number;
      
            write(file: string, content: string): void;
      
          }
      
          export interface Optional {
      
            name: string;
      
            detect(uniqueKey: string, callback: (detached: Detached) => void): void;
      
          }
      
          export interface Detached {
      
            timestamp: number;
            totalSize?: number;
      
            applyTo(mainDrive: Drive, callback: Detached.CallbackWithShadow): void;
      
            purge(callback: Detached.CallbackWithShadow): void;
      
          }
      
          export module Detached {
            export interface CallbackWithShadow {
      
              (loaded: Shadow): void;
              progress?: (current: number, total: number) => void;
            }
          }
      
        }
      }
    • bestEncode.ts
      module persistence {
      
        export function bestEncode(content: any, escapePath?: boolean): { content: string; encoding: string; } {
      
          if (content.length>1024*16) {
            // TODO: consider packing tightly and using eval encoding to unpack
          }
      
          if (typeof content!=='string')
            return { content: encodeArrayOrSimilarAsJSON(content), encoding: 'json' };
      
          var needsEscaping: boolean;
          if (escapePath) {
            // zero-char, newlines, leading/trailing spaces, quote and apostrophe
            needsEscaping = /\u0000|\r|\n|^\s|\s$|\"|\'/.test(content);
          }
          else {
            needsEscaping = /\u0000|\r/.test(content);
          }
      
          if (needsEscaping) {
            // ZERO character is officially unsafe in HTML,
            // CR is contentious in IE (which converts any CR or LF into CRLF)
      
            return { content: encodeUnusualStringAsJSON(content), encoding: 'json' };
          }
          else {
            return { content: content, encoding: 'LF' };
          }
        }
      
        function encodeUnusualStringAsJSON(content: string): string {
          if (typeof JSON !== 'undefined' && typeof JSON.stringify === 'function') {
            var simpleJSON = JSON.stringify(content);
            var sanitizedJSON = simpleJSON.
              replace(/\u0000/g, '\\u0000').
              replace(/\r/g, '\\r').
              replace(/\n/g, '\\n');
            return sanitizedJSON;
          }
          else {
            var result = content.replace(
              /\"\u0000|\u0001|\u0002|\u0003|\u0004|\u0005|\u0006|\u0007|\u0008|\u0009|\u00010|\u00011|\u00012|\u00013|\u00014|\u00015|\u0016|\u0017|\u0018|\u0019|\u0020|\u0021|\u0022|\u0023|\u0024|\u0025|\u0026|\u0027|\u0028|\u0029|\u0030|\u0031/g,
              (chr) =>
                chr === '\t' ? '\\t' :
                  chr === '\r' ? '\\r' :
                    chr === '\n' ? '\\n' :
                      chr === '\"' ? '\\"' :
                        chr < '\u0010' ? '\\u000' + chr.charCodeAt(0).toString(16) :
                          '\\u00' + chr.charCodeAt(0).toString(16));
            return result;
          }
        }
      
        function encodeArrayOrSimilarAsJSON(content: any): string {
            var type = content instanceof Array ? null : content.constructor.name || content.type;
            if (typeof JSON !== 'undefined' && typeof JSON.stringify === 'function') {
              if (type) {
                var wrapped = { type, content };
                var wrappedJSON = JSON.stringify(wrapped);
                return wrappedJSON;
              }
              else {
                var contentJSON = JSON.stringify(content);
                return contentJSON;
              }
            }
            else {
              var jsonArr: string[] = [];
              if (type) {
                jsonArr.push('{"type": "');
                jsonArr.push(content.type || content.prototype.constructor.name);
                jsonArr.push('", "content": [');
              }
              else {
                jsonArr.push('[');
              }
      
              for (var i = 0; i < content.length; i++) {
                if (i) jsonArr.push(',');
                jsonArr.push(content[i]);
              }
      
              if (type)
                jsonArr.push(']}');
              else
                jsonArr.push(']');
      
              return jsonArr.join('');
            }
        }
      }
    • bootMount.ts
      module persistence {
      
        // TODO: pass in progress callback
        export function bootMount(uniqueKey: string, document: Document): bootMount.ContinueLoading {
      
          var continueParse: persistence.dom.parseDOMStorage.ContinueParsing;
      
          var ondomdriveloaded;
          var domDriveLoaded: Drive;
          var storedFinishCallback;
      
          mountDrive(
            callback => {
              if (domDriveLoaded)
                callback(domDriveLoaded);
              else
                ondomdriveloaded = callback;
            },
            uniqueKey,
            [attached.indexedDB, attached.webSQL, attached.localStorage],
            mountedDrive => {
      
              storedFinishCallback(mountedDrive);
      
            });
      
          return continueLoading();
      
          function continueLoading(): bootMount.ContinueLoading {
      
            continueDOMLoading();
      
            // TODO: record progress
      
            return { continueLoading, finishLoading };
          }
      
          function finishLoading(finishCallback: (monutedDrive: Drive) => void) {
      
            storedFinishCallback = finishCallback;
      
            continueDOMLoading();
      
            domDriveLoaded = continueParse.finishParsing();
      
            if (ondomdriveloaded) {
              ondomdriveloaded(domDriveLoaded);
            }
      
          }
      
      
          function continueDOMLoading() {
            continueParse = continueParse ? continueParse.continueParsing() : dom.parseDOMStorage(document);
          }
      
        }
      
        module bootMount {
      
          export interface ContinueLoading {
      
            continueLoading(): ContinueLoading;
      
            finishLoading(finishCallback: (mountedDrive: Drive) => void);
      
          }
      
        }
      }
    • mountDrive.ts
      module persistence {
      
        export function mountDrive(
          loadDOMDrive: (callback: (dom: Drive) => void)=> void,
          uniqueKey: string,
          optionalModules: Drive.Optional[],
          callback: mountDrive.Callback): void {
      
          var driveIndex = 0;
      
          loadNextOptional();
      
          function loadNextOptional() {
      
            while (driveIndex < optionalModules.length &&
              (!optionalModules[driveIndex] || typeof optionalModules[driveIndex].detect !== 'function')) {
              driveIndex++;
            }
      
            if (driveIndex >= optionalModules.length) {
              loadDOMDrive(dom => callback(new MountedDrive(dom, null)));
              return;
            }
      
            var op = optionalModules[driveIndex];
            op.detect(
              uniqueKey,
              detached => {
                if (!detached) {
                  driveIndex++;
                  loadNextOptional();
                  return;
                }
      
                loadDOMDrive(dom => {
                  if (detached.timestamp > dom.timestamp) {
                    var callbackWithShadow: Drive.Detached.CallbackWithShadow = loadedDrive => {
                      dom.timestamp = detached.timestamp;
                      callback(new MountedDrive(dom, loadedDrive));
                    };
                    if (callback.progress)
                      callbackWithShadow.progress = callback.progress;
                    loadDOMDrive(dom => detached.applyTo(dom, callbackWithShadow));
                  }
                  else {
                    var callbackWithShadow: Drive.Detached.CallbackWithShadow = loadedDrive => {
                      callback(new MountedDrive(dom, loadedDrive));
                    };
                    if (callback.progress)
                      callbackWithShadow.progress = callback.progress;
                    detached.purge(callbackWithShadow);
                  }
                });
      
              });
          }
      
        }
      
        export module mountDrive {
      
          export interface Callback {
      
            (drive: Drive): void;
      
            progress?: (current: number, total: number) => void;
      
          }
      
        }
      
        class MountedDrive implements Drive {
      
          timestamp: number = 0;
      
          constructor (private _dom: Drive, private _shadow: Drive.Shadow) {
            this.timestamp = this._dom.timestamp;
          }
      
          files(): string[] {
            return this._dom.files();
          }
      
          read(file: string): string {
            return this._dom.read(file);
          }
      
          write(file: string, content: string) {
            this._dom.timestamp = this.timestamp;
            this._dom.write(file, content);
            if (this._shadow) {
              this._shadow.timestamp = this.timestamp;
              this._shadow.write(file, content);
            }
          }
        }
      
      }
    • normalizePath.ts
      module persistence {
      
        export function normalizePath(path: string) : string {
      
          if (!path) return '/'; // empty paths converted to root
      
          while (' \n\t\r'.indexOf(path.charAt(0))>=0) // removing leading whitespace
            path = path.slice(1);
      
          while ('\n\t\r\\'.indexOf(path.charAt(path.length - 1))>=0) // removing trailing whitespace and trailing slashes
            path = path.slice(0, path.length - 1);
      
          if (path.charAt(0) !== '/') // ensuring leading slash
            path = '/' + path;
      
          path = path.replace(/\/\/*/g, '/'); // replacing duplicate slashes with single
      
          return path;
        }
      
      }
  • typings
    • webSQL.d.ts
      declare function openDatabase(
        name: string,
        version: any,
        displayName: string,
        size: number,
        upgrade?: DatabaseCallback): Database;
      
      interface DatabaseCallback {
        (database: Database): void;
      }
      
      interface Database {
        transaction(
          callback: (transaction: SQLTransaction) => void,
          errorCallback?: (error: SQLError) => void,
          successCallback?: () => void);
      
        readTransaction(
          callback: (transaction: SQLTransaction) => void,
          errorCallback?: (error: SQLError) => void,
          successCallback?: () => void);
      
        version: string;
      
        changeVersion(
          oldVersion: string,
          newVersion: string,
          callback: (transaction: SQLTransaction) => void,
          errorCallback?: (error: SQLError) => void,
          successCallback?: () => void);
      }
      
      interface SQLTransaction {
        executeSql(
          sqlStatement: string,
          arguments?: any[],
          callback?: (transaction: SQLTransaction, result: SQLResultSet) => void,
          errorCallback?: (transaction: SQLTransaction, error: SQLError) => void): void;
      }
      
      interface SQLError {
        /**
         * UNKNOWN_ERR = 0;
         * DATABASE_ERR = 1;
         * VERSION_ERR = 2;
         * TOO_LARGE_ERR = 3;
         * QUOTA_ERR = 4;
         * SYNTAX_ERR = 5;
         * CONSTRAINT_ERR = 6;
        * TIMEOUT_ERR = 7;
         */
        code: number;
        message: string
      }
      
      interface SQLResultSet {
        insertId: number;
        rowsAffected: number;
        rows: SQLResultSetRowList;
      }
      
      interface SQLResultSetRowList {
        length: number;
        item(index: number): any;
      }
  • index.html
    <!doctype html>
    <title>mini shell </title>
    
    <script data-legit=mi>
      <%=embedFile('boot/onerror.js')%>
    //# sourceURL=boot/onerror.js
    </script>
    <script data-legit=mi>
      earlyBoot();
      <%=embedFile('boot/base.js')%>
      <%=embedFile('boot/earlyBoot.js')%>
      <%=embedFile('boot/bootUI.js')%>
    //# sourceURL=boot/*.js
    </script>
    <script data-legit=mi>
    <%=typescriptBuild()%>
    //# sourceURL=typescriptBuild.ts
    </script>
    
    <!-- total 56Mb, saved 25 Apr 2015 22:52:01.231 -->
    
    <!-- /LF-file.txt
    123-->
    
    <!-- /src/LF-file.txt
    123-->
    
    <!-- /src1/LF-file.txt
    123-->
    
    <!-- /CRLF-file (CRLF)
    123
              4-->
    
    <!-- /eval-file.txt (eval)
    "new ok" + String.fromCharCode(Math.random()*16000)+" and what\r\n or "+String.fromCharCode(1)+"?"-->
    
    <!-- /json-file.txt (json)
    "new ok \u2222 and what\r\n or \u0001?"-->
    
    <!-- /runme.js
    console.log(123)-->
  • readme.md
    #Mini-portable shell - new

portabled v0.6.1a

Self-editing filesystem embedded in a single HTML file.

The idea, all of the painstaking implementation and the vision by Oleg Mihailik. See the credits section for the used libraries and respective licences.

Outstanding tasks:

  • Unifying of all import/export into 'moreDialog'.
  • Download/upload for GitHub, GDrive, Dropbox etc.
  • Extra power in Chrome app, node-webkit, HTMLA-ie7: I/O to the actual filesystem.
  • Delete folder.
  • Rename file/folder.
  • Saving current position in documents.
  • TypeScript extra features: navigate to, search integration, tooltips.
  • Sub-domains for TypeScript/JavaScript completion/build contexts.
  • Doc handlers in plugins, plugin API and isolation (using iframes with their own 'global' and 'require').
  • node.js emulation for plugins and dependencies, allowing non-doc plugins.
  • Highlight of changes in files.
  • Styles and colours (planning for pale seaside 'Whitstable' blue, maybe black theme too).
  • Scrollbar to use syntax-highlighted document lines.
  • Toast popup/fadeout messages for key events: opening, building, import-export completion.
  • Add whole raw TypeScript repository sample.
portabled v0.6.1a by Oleg Mihailik
built Sat May 02 2015 12:40:49 GMT+0100 (GMT Summer Time)

Used Open Source libraries:
TypeScript (Microsoft, with Apache 2.0 license)
CodeMirror (Marijn Haverbeke, with MIT license)
Knockout.js (Ryan Niemeyer, with MIT license)
Zip.js (Gildas Lormeau, with BSD license)
Marked (Christopher Jeffrey, with MIT license)
ES5-shims (with MIT license)
GitHub API wrapper (Michael Aufreiter with BSD2 license)
JS Murmur hasher (Gary Court with MIT license)
google-diff-match-patch (Google with Apache 2.0 license)
UglifyJS2 (Mihai Bazon with BSD license)
UglifyCSS (Franck Marcia with MIT license)
JSON3 (Kit Cambridge with MIT license)
- main contributors mentioned where applicable.
/readme.md